# 第八章 后端商品库管理

本章节我们将学习如何实现关于商品库管理的接口,商品是业务的基础,现实中的销售、活动、促销、宣传都是围绕着商品进行的,我们经常需要对商品进行上、下架,修改价格,关联某个活动或者促销等等,本章节就将带着大家一起学习如何实现一系列满足日常管理需求的接口。

# 分页查询所有商品

要管理商品,首先肯定就是要先查询出商品,我们的商品数量随着业务的增长会越来越多,如果按照前面我们的查询方式来查询,当商品数量不多的时候是可以的,但是当商品数量到达一定程度,比如一次性查询出了几百个商品,先不考虑性能什么的问题,光是前端CMS在展示数据的时候体验就会很差,所以一般都需要对这种存在比较多记录的查询做分页查询。在明确了实现思路之后,让我们开始进入套路时间,在控制层下,新增一个控制器类Product,并在控制器类中新增一个getProductsPaginate()方法:

<?php


namespace app\api\controller\v1;


class Product
{
    /**
     * 查询所有商品,分页
     */
    public function getProductsPaginate()
    {

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

控制器方法定义好之后,我们就需要来实现分页查询的逻辑了,考虑到实现的逻辑以后可能会复用,这里我们把实现的逻辑封装到模型类中去,在前面我们创建的Product模型中新增一个getProductsPaginate()方法,加入如下代码:

<?php


namespace app\api\model;


class Product extends BaseModel
{
    protected $hidden = ['delete_time', 'create_time', 'update_time', 'from'];


    public static function getProductsPaginate($params)
    {
        $product = [];
        // 判断是否传递了product_name参数,如果有,构造一个查询条件,按商品名称模糊查询
        if (array_key_exists('product_name', $params)) {
            $product[] = ['name', 'like', '%' . $params['product_name'] . '%'];
        }
        // paginate()方法用于根据url中count和page的参数,计算查询要查询的开始位置和查询数量
        list($start, $count) = paginate();
        // 拿到应用查询条件后的模型实例
        $productList = self::where($product);
        // 调用模型的实例方法count计算该条件下会有多少条记录
        $totalNums = $productList->count();
        // 调用模型的limit方法对记录进行分页并获取查询结果
        $productList = $productList->limit($start, $count)
            ->order('create_time desc')
            ->select();
        // 组装返回结果,这里与lin-cms风格保持一致
        $result = [
            // 查询结果
            'collection' => $productList,
            // 总记录数
            'total_nums' => $totalNums
        ];

        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

这里我们封装了一个方法并添加了一堆你可能不知道在干嘛的代码,接下来就让作者带着大家来剖析一下。

    public static function getProductsPaginate($params)
    {
        $product = [];
        // 判断是否传递了product_name参数,如果有,构造一个查询条件,按商品名称模糊查询
        if (array_key_exists('product_name', $params)) {
            $product[] = ['name', 'like', '%' . $params['product_name'] . '%'];
        }
    }
1
2
3
4
5
6
7
8

首先我们定义了一个getProductsPaginate()方法,方法接收一个$params参数,参数内容就是查询条件,包含count(每次查询的数量)、page(查询的页数)、product_name(商品名称),接着我们定义了一个$product变量,默认是一个空数组。接着我们对$params参数进行判断,判断参数中是否传递了product_name,如果有,就赋值给$product,这个变量的作用是构造查询条件,条件的内容是一个模糊查询,因为有时候我们在商品列表页面可能会单独搜索某个商品。这里我们调用了lin-cms-tp5内置的一个公共方法paginate()

        list($start, $count) = paginate();
1

这个方法用于返回分页查询时需要的参数,查询开始的位置和每次查询的数量,该方法的具体实现可查阅项目根目录下的application\common.php

这里我们需要的参数就基本准备妥当了,就可以开始进行查询了。

        // 构造条件查询
        $productList = self::where($product);
        // 调用count方法计算该条件下会有多少条记录
        $totalNums = $productList->count();
        // 调用模型的limit方法对记录进行分页并获取查询结果
        $productList = $productList->limit($start, $count)
            ->order('create_time desc')
            ->select();
1
2
3
4
5
6
7
8

这里我们首先调用了模型的where()方法,把前面的$product传递进去,这里的$product要么是一个空数组,要么是[['product_name'=>'参数值']],如果是空的,得到的结果将会是所有记录,反之就是作用查询条件后的记录。构造完查询条件之后,我们需要一个查询结果的总数(前端在做分页展示的时候,都有会一个总记录数的显示),这里调用count()方法来获取记录的总条目数。最后要做的就是真正的分页查询操作了,这里我们调用limit()方法,把前面我们通过paginate()计算出来的$start和$count传递进去进行分页,最后调用select()查询最终结果。

        // 组装返回结果,这里与lin-cms风格保持一致
        $result = [
            // 查询结果
            'collection' => $productList,
            // 总记录数
            'total_nums' => $totalNums
        ];

        return $result;
1
2
3
4
5
6
7
8
9

在前面拿到分页查询结果之后,这里就是拼装返回格式,这里格式理论上没有什么统一标准,按照项目情况和前端约定好即可,这里我们参考lin-cms中分页查询相关模块的返回格式来保持风格统一。

定义完模型的方法之后,让我们回到控制器方法中来调用一下:

<?php


namespace app\api\controller\v1;

use app\api\model\Product as ProductModel;
use app\lib\exception\product\ProductException;
use think\facade\Request;

class Product
{
    /**
     * 查询所有商品,分页
     * @param('page','查询页码','require|number')
     * @param('count','单页查询数量','require|number|between:1,15')
     */
    public function getProductsPaginate()
    {
        $params = Request::get();
        $products = ProductModel::getProductsPaginate($params);
        if ($products['total_nums'] === 0) {
            throw new ProductException([
                'code' => 404,
                'msg' => '未查询到相关商品',
                'error_code' => '70006'
            ]);
        }
        return $products;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

首先获取下查询参数,同样通过Request类的get()方法,然后在控制器方法中调用一下我们刚刚封装好的模型方法,并发参数传递进去。这里给接口定义了两个注解参数验证,要求传递查询的页码和数量。然后按照惯例,我们判断一下当返回结果的总数为0的时候抛出一个404的异常并给予文字提示。接着就是最后一步定义路由,打开router.php,新增一个路由分组product,在分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            // 分页查询所有商品
            Route::get('paginate', 'api/v1.Product/getProductsPaginate');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

路由定义好了之后就可以到Postman中愉快的进行测试了,打开Postman,按路由信息新增并配置好一个请求:

点击发送:

{
    "collection": [
        {
            "id": 1,
            "name": "芹菜 半斤",
            "price": "0.01",
            "stock": 998,
            "category_id": 3,
            "main_img_url": "http://localhost:8000/images/product-vg@1.png",
            "status": 1,
            "summary": null,
            "img_id": 13
        },
        {
            "id": 25,
            "name": "小明的妙脆角 120克",
            "price": "0.01",
            "stock": 999,
            "category_id": 5,
            "main_img_url": "http://localhost:8000/images/product-cake@1.png",
            "status": 1,
            "summary": null,
            "img_id": 52
        },
        {
            "id": 2,
            "name": "梨花带雨 3个",
            "price": "0.01",
            "stock": 984,
            "category_id": 2,
            "main_img_url": "http://localhost:8000/images/product-dryfruit@1.png",
            "status": 1,
            "summary": null,
            "img_id": 10
        }
    ],
    "total_nums": 32
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

没有报错,接口按照我们设定的格式返回了数据,这里我们从第0页开始查询,一次查询3条记录,从返回的结果中的total_nums属性的值可以看出,我们商品的总记录数是32,我们可以通过传递page参数来查看其他页的数据,这里再我们通过修改page参数,来查询下一页的数据:

再次发送请求,得到结果:

{
    "collection": [
        {
            "id": 3,
            "name": "素米 327克",
            "price": "0.01",
            "stock": 996,
            "category_id": 7,
            "main_img_url": "http://localhost:8000/images/product-rice@1.png",
            "status": 1,
            "summary": null,
            "img_id": 31
        },
        {
            "id": 4,
            "name": "红袖枸杞 6克*3袋",
            "price": "0.01",
            "stock": 998,
            "category_id": 6,
            "main_img_url": "http://localhost:8000/images/product-tea@1.png",
            "status": 1,
            "summary": null,
            "img_id": 32
        },
        {
            "id": 5,
            "name": "春生龙眼 500克",
            "price": "0.01",
            "stock": 995,
            "category_id": 2,
            "main_img_url": "http://localhost:8000/images/product-dryfruit@2.png",
            "status": 1,
            "summary": null,
            "img_id": 33
        }
    ],
    "total_nums": 32
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

可以看到数据不一样了,这里我们的分页查询商品的功能就已经实现了。不过仔细观察下返回的数据,这里的category_id标识了这个商品是属于哪个分类的,但是并没有显示出具体的分类信息,这显然不是我们想要的。另外通过观察数据库,商品表还另外关联了两张表,一个是product_image,用于表示商品详情图片,另外一个是product_property,用于表示商品属性。也就是说商品表一共关联了另外3张表,有别于其他使用场景,脑补了前端在管理商品时的页面,我们需要全部有关商品的信息,所以这里我们通过关联查询,来把这些信息一并查询出来,回到我们Product模型下,定义三个声明关联关系的方法:

<?php


namespace app\api\model;


class Product extends BaseModel
{
    protected $hidden = ['delete_time', 'create_time', 'update_time', 'from'];


    public static function getProductsPaginate($params){...}

    public function category()
    {
        // 相对关联
        return $this->belongsTo('Category');
    }

    public function image()
    {
        // 一对多
        // product_image表中有order字段用于图片显示排序
        return $this->hasMany('ProductImage')->order('order');
    }

    public function property()
    {
        // 一对多
        return $this->hasMany('ProductProperty');
    }

    public function getMainImgUrlAttr($value, $data){...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这里还需要另外在模型层目录下创建ProductImage和ProductProperty模型类

这里我们通过category()image()property()三个方法声明了Product模型与其他三个模型的关联关系,定义好了方法,我们就可以在查询的时候调用一下了,修改一下getProductsPaginate()模型方法,在select()之前调用一下with()方法:

public static function getProductsPaginate($params)
    {
        ..........................................
        ..........................................
        // 调用模型的limit方法对记录进行分页并获取查询结果
        $productList = $productList->limit($start, $count)
            ->with('category,image.img,property')
            ->order('create_time desc')
            ->select();
        // 组装返回结果
        $result = [
            'collection' => $productList,
            'total_nums' => $totalNums
        ];

        return $result;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意,这里调用image关联方法时使用了嵌套关联,因为product_image表里有img_id字段,我们在查询的时候肯定是希望看到对应的图片的,所以在创建ProductImage模型时,需要为它定义一个关联Image模型的关联声明:

<?php


namespace app\api\model;


use think\model\concern\SoftDelete;

class ProductImage extends BaseModel
{
    use SoftDelete;

    protected $hidden = ['delete_time', 'product_id'];

    public function img()
    {
        return $this->belongsTo('Image', 'img_id');
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

修改完毕之后,让我们然后回到Postman中,我们再次发起请求:

{
    "collection": [
        {
            "id": 3,
            "name": "素米 327克",
            "price": "0.01",
            "stock": 996,
            "category_id": 7,
            "main_img_url": "http://localhost:8000/images/product-rice@1.png",
            "status": 1,
            "summary": null,
            "img_id": 31,
            "category": null,
            "image": [],
            "property": []
        },
        {
            "id": 4,
            "name": "红袖枸杞 6克*3袋",
            "price": "0.01",
            "stock": 998,
            "category_id": 6,
            "main_img_url": "http://localhost:8000/images/product-tea@1.png",
            "status": 1,
            "summary": null,
            "img_id": 32,
            "category": {
                "id": 6,
                "name": "粗茶",
                "description": "阿萨德"
            },
            "image": [],
            "property": []
        },
        {
            "id": 5,
            "name": "春生龙眼 500克",
            "price": "0.01",
            "stock": 995,
            "category_id": 2,
            "main_img_url": "http://localhost:8000/images/product-dryfruit@2.png",
            "status": 1,
            "summary": null,
            "img_id": 33,
            "category": {
                "id": 2,
                "name": "水果",
                "description": ""
            },
            "image": [],
            "property": []
        }
    ],
    "total_nums": 32
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

这里可以看到我们的返回结果中多出了一些字段,由于素材中很多商品没有添加详情图片和属性所以对应的字段是一个空数组,这里我们可以尝试查询下其他页的数据,比如第3页,商品id为11的记录:

{
    "id": 11,
    "name": "贵妃笑 100克",
    "price": "0.01",
    "stock": 994,
    "category_id": 2,
    "main_img_url": "http://localhost:8000/images/product-dryfruit-a@6.png",
    "status": 1,
    "summary": null,
    "img_id": 39,
    "category": {
        "id": 2,
        "name": "水果",
        "description": ""
    },
    "image": [
        {
            "id": 4,
            "img_id": 19,
            "order": 1,
            "img": {
                "id": 19,
                "url": "http://localhost:8000/images/detail-1@1-dryfruit.png"
            }
        },
        {
            "id": 5,
            "img_id": 20,
            "order": 2,
            "img": {
                "id": 20,
                "url": "http://localhost:8000/images/detail-2@1-dryfruit.png"
            }
        },
        {
            "id": 6,
            "img_id": 21,
            "order": 3,
            "img": {
                "id": 21,
                "url": "http://localhost:8000/images/detail-3@1-dryfruit.png"
            }
        },
        {
            "id": 7,
            "img_id": 22,
            "order": 4,
            "img": {
                "id": 22,
                "url": "http://localhost:8000/images/detail-4@1-dryfruit.png"
            }
        },
        {
            "id": 8,
            "img_id": 23,
            "order": 5,
            "img": {
                "id": 23,
                "url": "http://localhost:8000/images/detail-5@1-dryfruit.png"
            }
        },
        {
            "id": 9,
            "img_id": 24,
            "order": 6,
            "img": {
                "id": 24,
                "url": "http://localhost:8000/images/detail-6@1-dryfruit.png"
            }
        },
        {
            "id": 10,
            "img_id": 25,
            "order": 7,
            "img": {
                "id": 25,
                "url": "http://localhost:8000/images/detail-7@1-dryfruit.png"
            }
        },
        {
            "id": 11,
            "img_id": 26,
            "order": 8,
            "img": {
                "id": 26,
                "url": "http://localhost:8000/images/detail-8@1-dryfruit.png"
            }
        },
        {
            "id": 12,
            "img_id": 27,
            "order": 9,
            "img": {
                "id": 27,
                "url": "http://localhost:8000/images/detail-9@1-dryfruit.png"
            }
        },
        {
            "id": 14,
            "img_id": 29,
            "order": 10,
            "img": {
                "id": 29,
                "url": "http://localhost:8000/images/detail-10@1-dryfruit.png"
            }
        },
        {
            "id": 13,
            "img_id": 28,
            "order": 11,
            "img": {
                "id": 28,
                "url": "http://localhost:8000/images/detail-11@1-dryfruit.png"
            }
        },
        {
            "id": 18,
            "img_id": 62,
            "order": 12,
            "img": {
                "id": 62,
                "url": "http://localhost:8000/images/detail-12@1-dryfruit.png"
            }
        },
        {
            "id": 19,
            "img_id": 63,
            "order": 13,
            "img": {
                "id": 63,
                "url": "http://localhost:8000/images/detail-13@1-dryfruit.png"
            }
        }
    ],
    "property": [
        {
            "id": 1,
            "name": "品名",
            "detail": "杨梅"
        },
        {
            "id": 2,
            "name": "口味",
            "detail": "青梅味 雪梨味 黄桃味 菠萝味"
        },
        {
            "id": 3,
            "name": "产地",
            "detail": "火星"
        },
        {
            "id": 4,
            "name": "保质期",
            "detail": "180天"
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157

可以看到,有关商品的所有信息都可以通过这个接口全部查询到了,到这里我们的分页查询所有商品的接口就算实现完毕了。

扩展知识:
分页查询是一种常见的查询方式,主要在解决查询较大数据量时使用,比如我们平时使用APP时常见的下拉加载更多,cms中表格的分页显示等等,这些都是通过分页查询实现的。分页查询本质上是对数据的一种切片,就是分块查询,通过本小节学习,读者可能会有一个疑问就是实现的过程其实也不是很复杂,直接丢给前端来实现可以吗?答案是可以的,但是性价比很低。相对于后端实现分页查询,前端来实现数据分页方法大同小异,但问题的关键点在于如果由前端来实现,意味着这个接口要返回大量的数据,然后前端才能根据这部分数据来做处理。这里可以想象下你平时对一个应用的页面内容你最多会翻多少页?我相信大部分人都不会翻那么多页。所以为了那有限的几次翻页操作让接口一次性返回大量的数据,前端和后端都会产生很大的性能开销,实在划不来。这里同时也延伸出了一个我们在做前后端分离项目时一个常见的现象,就是xxx功能前端还是后端来实现的问题,这里除去推诿和能力问题,一般都需要从复用性和性价比两个基本面去考虑,最理想的情况当然是后端返回什么,前端就能拿来直接用,但现实是越具体的接口,也就代表它越与某个业务的实现耦合,这个接口的复用性和扩展性、灵活性就降低了,所以往往我们设计接口的时候,其实现的内容多数情况下相对没那么具体,前端常常需要对接口返回的数据进行清洗就是这个原因,但实现的效果可能就是前端可以通过清洗一个接口的数据实现多个功能模块,性价比就体现在这里。当然这里要展开说的话,内容远不止这些,这里仅做一点扩展学习,在后续章节内容中我们还会涉及到这方面的思考,读者可以关注学习。如果日后在实际工作中遇到了这种分歧,不妨按照这里的分析思路尝试解决。

# 查询所有可用商品

本小节我们将学习查询商品的另一种姿势,本小节要实现的接口与上一小节中实现的接口不同之处在于可用二字。在上一小节中,我们查询出来的商品记录是product表中所有的记录,这个场景适用于在cms中管理商品的页面,我们会在这个页面中对商品做一些操作,比如典型的上架/下架操作。而本小节要实现的接口的使用场景就好比在管理精选主题的时候,我们要给精选主题分配所包含的商品,那这时候肯定要有个接口返回全部商品的列表以供选择,而且商品还是得都可用的,什么叫可用?就是没有被下架的商品,一个已经被下架了的商品当然不能用于分配到其他业务模块中,这就是两者使用场景的区别。那么问题来了,怎么判断一个商品的状态是可用的?通过观察素材数据库中的product表我们会发现并没有相关的字段,为了后续的学习和业务实现,这里我们需要手动给product表增加一个字段status,我们通过该字段来标识一个商品的状态是已下架(0)还是上架中(1):

字段我们有了,接下来操作就是熟悉的操作了。打开项目中控制层下的Product控制器类,新增一个getProducts()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Product as ProductModel;
use app\lib\exception\product\ProductException;
use think\facade\Request;

class Product
{
    /** 查询所有商品,分页*/
    public function getProductsPaginate(){...}

    /**
     * 查询所有可用商品,用于给前端某些功能的选项列表使用
     */
    public function getProducts()
    {
        $products = ProductModel::where('status', 1)->select();
        if ($products->isEmpty()) {
            throw new ProductException([
                'code' => 404,
                'msg' => '未查询到相关商品'
            ]);
        }
        return $products;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在查询出结果之前调用where()方法,条件为status等于1的记录,这样已下架的商品就不会查询被查询出来,接下来让我们来测试一下结果是否符合我们的预期,打开router.php,在product路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            // 分页查询所有商品
            Route::get('paginate', 'api/v1.Product/getProductsPaginate');
            // 查询所有可用商品
            Route::get('', 'api/v1.Product/getProducts');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

路由定义好之后,打开Postman并按路由规则配置请求:

在发送请求之前,我们需要制造点测试数据,这里我把id为1的商品它的状态字段值改为0,就是已下架,然后点击发送请求:

[
    {
        "id": 2,
        "name": "梨花带雨 3个",
        "price": "0.01",
        "stock": 984,
        "category_id": 2,
        "main_img_url": "http://localhost:8000/images/product-dryfruit@1.png",
        "status": 1,
        "summary": null,
        "img_id": 10
    },
    {
        "id": 3,
        "name": "素米 327克",
        "price": "0.01",
        "stock": 996,
        "category_id": 7,
        "main_img_url": "http://localhost:8000/images/product-rice@1.png",
        "status": 1,
        "summary": null,
        "img_id": 31
    },
    // ......省略内容
]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

可以看到这里查询出来的结果中id为1的记录已经不存在了,这样子我们前端页面在显示有关商品选项的列表时就不会出现这个商品了。但是这时候我们再考虑一个情况,假如我们先给一个精选主题分配了商品A,接着再把商品A下架了,当再次查询这个精选主题的时候,包含的商品会显示什么?让我们来测试一下,刚刚我们把id为1的商品状态改成了下架,在素材数据库中,初始状态下,id为1的精选主题下包含了这个商品,正好对应我们现在这个问题的情况,在Postman中找到我们前面小节创建好的查询指主题详情接口,查询id为1的精选主题,得到结果:

{
    "id": 1,
    "name": "专题栏位一",
    "description": "美味水果世界",
    "topic_img": {
        "id": 16,
        "url": "http://localhost:8000/images/1@theme.png"
    },
    "head_img": {
        "id": 49,
        "url": "http://localhost:8000/images/1@theme-head.png"
    },
    "products": [
        {
            "id": 1,
            "name": "芹菜 半斤",
            "price": "0.01",
            "stock": 998,
            "category_id": 3,
            "main_img_url": "http://localhost:8000/images/product-vg@1.png",
            "status": 0,
            "summary": null,
            "img_id": 13,
            "pivot": {
                "theme_id": 1,
                "product_id": 1
            }
        },
        {
            "id": 2,
            "name": "梨花带雨 3个",
            "price": "0.01",
            "stock": 984,
            "category_id": 2,
            "main_img_url": "http://localhost:8000/images/product-dryfruit@1.png",
            "status": 1,
            "summary": null,
            "img_id": 10,
            "pivot": {
                "theme_id": 1,
                "product_id": 2
            }
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

你会发现id为1的商品依然存在,虽然商品已经下架了,但是依然出现在运营或者促销的信息中,这从业务层面上来说肯定过不了关,那怎么解决这个问题呢?让我们回过头来看看我们的Theme模型类里的实现:

<?php


namespace app\api\model;

use think\Db;
use think\Exception;
use think\Model;
use think\model\concern\SoftDelete;

class Theme extends Model
{
    use SoftDelete;
    protected $hidden = ['topic_img_id', 'head_img_id', 'delete_time', 'update_time'];

    /**
     * @param $ids
     * @return bool
     */
    public static function delTheme($ids)
    {
        // 开启事务
        Db::startTrans();
        try {
            // 执行软删除
            self::destroy($ids);
            // 删除中间表中对应主题id的记录,注意这里是执行硬删除
            foreach ($ids as $id) {
                ThemeProduct::where('theme_id', $id)->delete();
            }
            Db::commit();
            return true;
        } catch (Exception $ex) {
            Db::rollback();
            return false;
        }
    }

    public function products()
    {
        return $this->belongsToMany('Product');

    }

    public function topicImg()
    {
        return $this->belongsTo('Image', 'topic_img_id', 'id');
    }

    public function headImg()
    {
        return $this->belongsTo('Image', 'head_img_id', 'id');
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

相信大家对Theme模型类里的实现还有印象,这里我们在模型内部定义了一个products()方法,该方法用于声明theme表与product表的多对多关系,我们通过关联查询来实现查询主题下包含的所有商品,解决问题的方法就是在这里。前面我们有简单提到过,模型关联的声明方法会返回被关联模型的对象,那么我们是否可以给这个返回对象添加一些操作来实现条件过滤呢?答案是可以的:

<?php

class Theme extends Model
{
    use SoftDelete;
    protected $hidden = ['topic_img_id', 'head_img_id', 'delete_time', 'update_time'];

    public static function delTheme($ids){...}

    public function products()
    {
        return $this->belongsToMany('Product')->where('status', '=', 1);
    }

    public function topicImg(){...}

    public function headImg(){...}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里我们让这个关联模型在返回之前调用一下where()方法并传入查询条件,这样子在做关联查询的时候,被关联模型就会过滤掉不符合查询条件的记录,有没有效果让我们来测试一下,再次调用一下查询指主题详情接口:

{
    "id": 1,
    "name": "专题栏位一",
    "description": "美味水果世界",
    "topic_img": {
        "id": 16,
        "url": "http://localhost:8000/images/1@theme.png"
    },
    "head_img": {
        "id": 49,
        "url": "http://localhost:8000/images/1@theme-head.png"
    },
    "products": [
        {
            "id": 2,
            "name": "梨花带雨 3个",
            "price": "0.01",
            "stock": 984,
            "category_id": 2,
            "main_img_url": "http://localhost:8000/images/product-dryfruit@1.png",
            "status": 1,
            "summary": null,
            "img_id": 10,
            "pivot": {
                "theme_id": 1,
                "product_id": 2
            }
        }
    ]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

诶?我们的芹菜同学消失了,这就是我们要的效果,现在我们的商品状态就能同步到其他业务模块中去了。

这里我们给product表额外添加了status用于标识该商品可不可用,如需要零食商贩小程序适配这种变化,需要调整零食商贩小程序后端API关于商品查询部分的实现,方法与本小节内实现的方式一致,就是在查询商品的相关操作添加where()条件查询过滤掉status值为0的记录。

# 商品上架/下架

在前面小节中,我们给product表新增了一个status字段,并通过修改它的字段值来测试可用商品查询接口的返回结果,对应到真实业务场景中,这个修改的操作其实就是商品的上架/下架处理,本小节我们就来手动来实现下这个接口,打开我们的Product控制器类,新增一个modifyStatus()控制器方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Product as ProductModel;
use app\lib\exception\product\ProductException;
use think\facade\Request;

class Product
{
    /** 查询所有商品,分页*/
    public function getProductsPaginate(){...}

    /** 查询所有可用商品,用于给前端某些功能的选项列表使用*/
    public function getProducts(){...}

    /**
     * 商品上架/下架
     * @param('id','商品id','require|number')
     */
    public function modifyStatus($id)
    {
        $product = ProductModel::get($id);
        if (!$product) {
            throw new ProductException([
                'code' => 404,
                'msg' => '未查询到相关商品',
                'error_code' => '70006'
            ]);
        }
        $product->status = !$product->status;
        $product->save();
        return writeJson(201, [], '状态已经修改');
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

这里我们要求该控制器方法接收一个$id参数,代表某个商品的id。利用Product模型查询出对应id的商品模型对象,接着我们给这个商品对象的status属性做取反操作并赋值,这样每次调用这个接口时,就会改变这个商品的状态(0和1在PHP中做取反操作会得到true和false,我们在设计status字段时候,指定了字段类型为tinyint,当写入true或者false的时候会自动变成1或者0)。这里我们同样对$id参数做了一个注解参数校验,同时当指定的id商品不存在时抛出一个自定义的异常信息。控制器方法定义好之后,打开router.php,在product路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            // 分页查询所有商品
            Route::get('paginate', 'api/v1.Product/getProductsPaginate');
            // 查询所有可用商品
            Route::get('', 'api/v1.Product/getProducts');
            // 商品上架/下架
            Route::patch(':id','api/v1.Product/modifyStatus');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

接着打开Postman,按照路由信息配置请求:

前面我们把id为1的商品状态改成了0,即已下架的状态,这里我们尝试再次改变它的状态,让这个商品变成上架中的状态,点击发送请求:

{
    "error_code": 0,
    "result": [],
    "msg": "状态已经修改"
}
1
2
3
4
5

没有报错,接着我们调用一下查询所有可用商品接口来验证一下:

[
    {
        "id": 1,
        "name": "芹菜 半斤",
        "price": "0.01",
        "stock": 998,
        "category_id": 3,
        "main_img_url": "http://localhost:8000/images/product-vg@1.png",
        "status": 1,
        "summary": null,
        "img_id": 13
    },
    {
        "id": 2,
        "name": "梨花带雨 3个",
        "price": "0.01",
        "stock": 984,
        "category_id": 2,
        "main_img_url": "http://localhost:8000/images/product-dryfruit@1.png",
        "status": 1,
        "summary": null,
        "img_id": 10
    },
    // 省略内容

]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

可以看到,这里id为1的商品记录又出现了,说明这个商品的status值已经被修改,我们可以愉快的操作商品上架/下架了,这里考虑到快乐只能属于某些人,所以我们给这个接口来个权限控制:

<?php


namespace app\api\controller\v1;

use app\api\model\Product as ProductModel;
use app\lib\exception\product\ProductException;
use think\facade\Request;

class Product
{
    /** 查询所有商品,分页*/
    public function getProductsPaginate(){...}

    /** 查询所有可用商品,用于给前端某些功能的选项列表使用*/
    public function getProducts(){...}

    /**
     * 商品上架/下架
     * @auth('商品上架/下架','商品管理')
     * @param('id','商品id','require|number')
     */
    public function modifyStatus($id)
    {
        ..........................
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

这里我们在控制器方法上面利用@auth()注解给这个接口指定了调用所需的权限名称和权限所属模块,这样快乐就只属于有权限的人了。

# 新增商品

在前面小节中我们实现了对商品的查询和上下架操作,目前素材库中只有32个商品,我们逐渐感觉到有点不够玩,我们想添加点神奇的商品进去,心动不如行动,打开我们项目控制层下的Product控制器类,新增一个控制器方法addProduct()并添加如下代码:

<?php


namespace app\api\controller\v1;

use app\api\model\Product as ProductModel;
use app\lib\exception\product\ProductException;
use think\facade\Request;

class Product
{
    /** 查询所有商品,分页*/
    public function getProductsPaginate(){...}
    /** 查询所有可用商品,用于给前端某些功能的选项列表使用*/
    public function getProducts(){...}
    /** 商品上架/下架*/
    public function modifyStatus($id){...}

    /**
     * 新增商品
     * @auth('新增商品','商品管理')
     * @validate('ProductForm')
     */
    public function addProduct()
    {
        $params = Request::post();
        // $params['main_img_url'] 是一个完整的url。
        // $array = explode(config('setting.img_prefix'), $params['main_img_url']);
        // $params['main_img_url'] = $array[1]
        $params['main_img_url'] = explode(config('setting.img_prefix'), $params['main_img_url'])[1];
        $product = ProductModel::create($params, true);
        if (!$product) {
            throw new ProductException([
                'msg' => '商品创建失败'
            ]);
        }
        $product->image()->saveAll($params['image']);
        $product->property()->saveAll($params['property']);

        return writeJson(201, [], '商品新增成功');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

这里还是复用我们前面的知识点,首先我们通过Request的post()方法获取提交的参数,然后调用Product模型的create()方法创建一个商品。因为新增商品的时候可能同时也配置了商品详情图和商品属性,所以在成功创建商品之后,调用一下我们前面定义好的两个关联模型方法实现关联新增。当然这里我们自然要对提交过来的数据做一个参数校验,同样使用注解验证器功能,这里我们创建一个验证器类ProductForm来实现参数校验,在项目根目录下的application\api\validate创建一个product文件夹,在这个文件夹下面新增一个ProductForm.php文件并加入如下代码:

<?php


namespace app\api\validate\product;


use LinCmsTp5\validate\BaseValidate;

class ProductForm extends BaseValidate
{
    protected $rule = [
        'name' => 'require',
        'category_id' => 'number',
        'img_id' => 'require|number',
        'main_img_url' => 'require|url',
        'price' => 'require|float',
        'stock' => 'require|number',
        'summary' => 'chsDash',
        'image' => 'array|productImage',
        'property' => 'array|productProperty',
    ];

    protected function productImage($value)
    {
        if (!empty($value)) {
            foreach ($value as $v) {
                if (!isset($v['img_id']) || empty($v['img_id'])) {
                    return '商品详情图不能为空';
                }
            }
        }
        return true;
    }

    protected function productProperty($value)
    {
        if (!empty($value)) {
            foreach ($value as $v) {
                if (!isset($v['name']) || empty($v['name'])) {
                    return '商品属性名称不能为空';
                }
                if (!isset($v['detail']) || empty($v['detail'])) {
                    return '商品属性' . $v['name'] . '的详情不能为空';
                }
            }
        }
        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

在这个验证器类里面,我们给提交的参数设定了一些内置的校验规则,另外针对image和property参数我们除了使用内置的array校验规则以外我们还分别为其实现了自定义验证规则,规则内容主要就是当参数的值不为空时一些字段参数的简单判空操作。读者可以根据实际情况或者自己的理解丰富一下自定义验证规则的验证逻辑。

定义完验证器类之后我们来给这个接口定义一个路由规则,打开router.php文件,在product路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            // 分页查询所有商品
            Route::get('paginate', 'api/v1.Product/getProductsPaginate');
            // 查询所有可用商品
            Route::get('', 'api/v1.Product/getProducts');
            // 商品上架/下架
            Route::patch(':id','api/v1.Product/modifyStatus');
            // 新增商品
            Route::post('','api/v1.Product/addProduct');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:

在下方空白的输入区域粘贴如下json数据:

{
	"name":"厕所",
	"price":"1",
	"category_id":2,
	"main_img_url":"http://localhost:8000/images/20190826/092e2f98d3f1d84f13b2a0fe96322018.png",
	"img_id":"145",
	"stock":"2",
	"summary":"",
	"status":0,
	"image":[
		{
			"img_id":"146",
			"order":0
		},
		{
			"img_id":"147",
			"order":1
			
		}],
    "property":[
        {
            "name":"性别",
            "detail":"不想"
        },
        {
            "name":"哈哈",
            "detail":"搜索"
        }]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这里我们尝试添加一个名叫厕所的商品,其他数据都是按照验证器的校验条件随意输入的,点击发送:

{
    "error_code": 0,
    "result": [],
    "msg": "商品新增成功"
}
1
2
3
4
5

没有报错,这里提示我们创建成功了,接着我们来调用一下分页查询接口验证一下是否能查询得到:

{
    "collection": [
        {
            "id": 1,
            "name": "芹菜 半斤",
            "price": "0.01",
            "stock": 998,
            "category_id": 3,
            "main_img_url": "http://localhost:8000/images/product-vg@1.png",
            "status": 1,
            "summary": null,
            "img_id": 13,
            "category": {
                "id": 3,
                "name": "蔬菜",
                "description": "撒大声地"
            },
            "image": [],
            "property": []
        },
        {
            "id": 25,
            "name": "小明的妙脆角 120克",
            "price": "0.01",
            "stock": 999,
            "category_id": 5,
            "main_img_url": "http://localhost:8000/images/product-cake@1.png",
            "status": 1,
            "summary": null,
            "img_id": 52,
            "category": {
                "id": 5,
                "name": "点心",
                "description": "奥术大师"
            },
            "image": [],
            "property": []
        },
        {
            "id": 2,
            "name": "梨花带雨 3个",
            "price": "0.01",
            "stock": 984,
            "category_id": 2,
            "main_img_url": "http://localhost:8000/images/product-dryfruit@1.png",
            "status": 1,
            "summary": null,
            "img_id": 10,
            "category": {
                "id": 2,
                "name": "水果",
                "description": ""
            },
            "image": [],
            "property": [
                {
                    "id": 5,
                    "name": "品名",
                    "detail": "梨子"
                },
                {
                    "id": 6,
                    "name": "产地",
                    "detail": "金星"
                },
                {
                    "id": 7,
                    "name": "净含量",
                    "detail": "100g"
                },
                {
                    "id": 8,
                    "name": "保质期",
                    "detail": "10天"
                }
            ]
        }
    ],
    "total_nums": 33
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

居然没有,难道我们的接口出问题了?但是仔细留意下total_nums属性会发现,这里的商品数量跟我们之前在实现分页查询接口的时候不一样,也就是说商品表中是有新增了一条记录的,我们可以到数据库中验证一下:

果然,在product表中,确实新增了一条记录,那这里的问题是出在哪了呢?大家可以回过来看看分页查询接口的实现逻辑,我们在查询的时候,在模型类的方法里面,我们对查询结果进行了排序,按照记录的创建时间即create_time字段来实现排序,由于素材库中默认商品这个字段的值都是空的,那么就会按照数据库中表记录的顺序来显示,所以刚刚我们在查询的时候第0页也就肯定查询不到了,那么这里我们想每次新增的商品在查询结果中都排在前面要怎么实现呢?答案就是每次新增商品的同时给该商品记录的create_time字段赋值,这样就可以实现按照创建时间排序。思路有了,实现方式也很简单,就是在新增的时候传递多一个参数,比如这样:

public function addProduct()
    {
        $params = Request::post();
        // $params['main_img_url'] 是一个完整的url。
        // $array = explode(config('setting.img_prefix'), $params['main_img_url']);
        // $params['main_img_url'] = $array[1]
        $params['main_img_url'] = explode(config('setting.img_prefix'), $params['main_img_url'])[1];
        $params['create_time'] = time();
        $product = ProductModel::create($params, true);
        if (!$product) {
            throw new ProductException([
                'msg' => '商品创建失败'
            ]);
        }
        $product->image()->saveAll($params['image']);
        $product->property()->saveAll($params['property']);

        return writeJson(201, [], '商品新增成功');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

很简单对吧,但是不需要。在真实开发场景中,绝大多数数据库表都会有类似create_timeupdate_timedelete_time等字段用于标识该记录的创建时间、修改时间和删除时间,当有新增、修改、删除操作时,你每次都要给这些字段做赋值操作这太麻烦了。在前面部分小节的学习中,我们使用了软删除这种东西,执行软删除之后,对应记录的delete_time字段就会自动填充一个时间戳,既然删除可以实现这种效果,新增和编辑可以吗?答案是可以的,只需要在模型类中增加一行代码,打开我们的Product模型类:

<?php


namespace app\api\model;


use think\model\concern\SoftDelete;

class Product extends BaseModel
{
    use SoftDelete;
    // 自动写入时间戳
    public $autoWriteTimestamp = true;
    protected $hidden = ['delete_time', 'create_time', 'update_time', 'from'];


    public static function getProductsPaginate($params){}

    public function category(){...}

    public function image(){...}

    public function property(){...}

    public function getMainImgUrlAttr($value, $data){...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

这里我们在模型类中增加了一行代码public $autoWriteTimestamp = true;,添加了这行代码之后,代表让模型操作实现自动写入时间戳功能,当我们执行模型的save()或者update()操作的时候,模型就会自动往表中的create_time和update_time字段写入一个时间戳。

更多关于自动时间戳的介绍点击查看(opens new window)

有了这个配置之后,我们再次新增一个商品,然后再次调用分页查询接口:

{
    "collection": [
        {
            "id": 42,
            "name": "有时间戳的厕所",
            "price": "1.00",
            "stock": 2,
            "category_id": 2,
            "main_img_url": "http://localhost:8000/images/20190826/092e2f98d3f1d84f13b2a0fe96322018.png",
            "status": 0,
            "summary": "",
            "img_id": 145,
            "category": {
                "id": 2,
                "name": "水果",
                "description": ""
            },
            "image": [
                {
                    "id": 30,
                    "img_id": 146,
                    "order": 0,
                    "img": {
                        "id": 146,
                        "url": "http://localhost:8000/images/20190826/4fe43f9b848d371a3c578580ca6ffd4b.png"
                    }
                },
                {
                    "id": 31,
                    "img_id": 147,
                    "order": 1,
                    "img": {
                        "id": 147,
                        "url": "http://localhost:8000/images/20190826/eb1d6c8844fd42e8adc63ac507017d38.png"
                    }
                }
            ],
            "property": [
                {
                    "id": 15,
                    "name": "性别",
                    "detail": "不想"
                },
                {
                    "id": 16,
                    "name": "哈哈",
                    "detail": "搜索"
                }
            ]
        },
        {
            "id": 1,
            "name": "芹菜 半斤",
            "price": "0.01",
            "stock": 998,
            "category_id": 3,
            "main_img_url": "http://localhost:8000/images/product-vg@1.png",
            "status": 1,
            "summary": null,
            "img_id": 13,
            "category": {
                "id": 3,
                "name": "蔬菜",
                "description": "撒大声地"
            },
            "image": [],
            "property": []
        },
        {
            "id": 25,
            "name": "小明的妙脆角 120克",
            "price": "0.01",
            "stock": 999,
            "category_id": 5,
            "main_img_url": "http://localhost:8000/images/product-cake@1.png",
            "status": 1,
            "summary": null,
            "img_id": 52,
            "category": {
                "id": 5,
                "name": "点心",
                "description": "奥术大师"
            },
            "image": [],
            "property": []
        }
    ],
    "total_nums": 34
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

可以看到,我们第二次新增的商品已经查询出来了,而且是排在了最前面,说明我们的排序已经生效了,这里我们可以再次观察下数据库中记录的变化:

这里模型为我们的记录自动写入了一个时间戳到create_timeupdate_time字段,如果是更新操作,则只会写入update_time字段。

推荐给每个带有时间字段的数据表对应的模型都添加上自动写入时间戳的功能。

# 删除商品

在前面小节中我们实现了商品新增的接口,然后利用接口给商品库中新增了两个“厕所”商品,在一堆吃的里面突然多出了这么两个奇怪的东西,有种不可描述的感觉,所以本小节的要学习的内容就是实现一个删除商品的接口,我们要把这两个奇怪的商品从我们的商品库中删除。事不宜迟,打开我们控制层下的Product控制器类,新增一个delProduct()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Product as ProductModel;
use app\lib\exception\product\ProductException;
use think\facade\Request;
use think\facade\Hook;

class Product
{
    /** 查询所有商品,分页*/
    public function getProductsPaginate(){...}
    /** 查询所有可用商品,用于给前端某些功能的选项列表使用*/
    public function getProducts(){...}
    /** 商品上架/下架*/
    public function modifyStatus($id){...}
    /**新增商品*/
    public function addProduct(){...}

    /**
     * 删除商品
     * @auth('删除商品','商品管理')
     * @param('ids','待删除的商品id列表','require|array|min:1')
     */
    public function delProduct()
    {
        $ids = Request::delete('ids');
        array_map(function ($id) {
            // get()方法第二个参数传入关联模型的方法名实现关联查询
            $product = ProductModel::get($id, 'image,property');
            // 如果product存在,做关联删除
            if ($product){
                // 在delete()之前调用together()并传入关联模型方法名实现关联删除
                $product->together('image,property')->delete();
            }
        }, $ids);
        Hook::listen('logger', '删除了id为' . implode(',', $ids) . '的商品');

        return writeJson(201, [], '商品删除成功');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

这里我们要求前端传递一个参数名为ids值为长度至少是1的数组,数组中是要删除的商品id。利用PHP内置的array_map()函数来给$ids数组里的每个元素作用一个方法,方法内的实现就是调用模型的关联删除,在删除一个商品的同时也把关联的商品属性和商品详情图一并删除。

注意这里涉及到三个模型的删除操作都是使用了软删除,需在对应的模型类中引用一下use SoftDelete;,如觉得没必要想直接硬删除也可以。

这里我们同样需要给删除商品接口加上接口权限的控制和行为日志的记录,定义好控制器方法之后,我们就来定义一条路由规则,打开route.php文件,在product路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            // 分页查询所有商品
            Route::get('paginate', 'api/v1.Product/getProductsPaginate');
            // 查询所有可用商品
            Route::get('', 'api/v1.Product/getProducts');
            // 商品上架/下架
            Route::patch(':id','api/v1.Product/modifyStatus');
            // 新增商品
            Route::post('','api/v1.Product/addProduct');
            // 删除商品
            Route::delete('','api/v1.Product/delProduct');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

路由规则定义好之后,打开Postman并按路由规则配置并新增一个请求:

这里读者要注意你的商品id可能与作者的不同,请自己先通过查询接口查询获取下相应的商品id。

这里我们尝试删除在上一小节中新增的两个奇怪商品,点击发送:

{
    "error_code": 0,
    "result": [],
    "msg": "商品删除成功"
}
1
2
3
4
5

没有报错,这里提示我们商品删除成功了,这时候当我们再次调用查询接口你就会发现那两个奇怪的商品已经消失不见了,同时相关联的商品属性和商品详情图在数据库表中的记录也被标识了已经被软删除。

# 编辑商品

通过前面几个小节的学习,我们实现了对商品的新增、上下架和删除的管理接口,然而有时候架不住一些小伙伴的蜜汁操作,比如说一个添加了174张详情图和250个属性的超精心制作商品,结果价格少输了个0然后商品成了爆款,这时候只能赶紧下架商品或者删除然后重头再来,这显然是个让人崩溃的解决方案,为了尽可能的挽救这种蜜汁小伙伴,我们需要实现一个编辑商品的接口,如果我们前面实现编辑类接口的思路一样,我们同样会把编辑操作拆分成几个细粒度的接口来让前端按需调用,由于一个商品本身涉及基础信息和商品详情图、商品属性所以接口数量会比较多,毕竟是本章节的最后一小节了,会说就多说点是吧:)。打开我们控制层下的Product控制器类,新增以下几个控制器方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Product as ProductModel;
use app\api\model\ProductImage as ProductImageModel;
use app\api\model\ProductProperty as ProductPropertyModel;
use app\lib\exception\product\ProductException;
use think\facade\Request;
use think\facade\Hook;

class Product
{
    /** 查询所有商品,分页*/
    public function getProductsPaginate(){...}
    /** 查询所有可用商品,用于给前端某些功能的选项列表使用*/
    public function getProducts(){...}
    /** 商品上架/下架*/
    public function modifyStatus($id){...}
    /**新增商品*/
    public function addProduct(){...}
    /** 删除商品*/
    public function delProduct(){...}

    /**
     * 更新商品基础信息
     * @validate('ProductForm.edit')
     */
    public function updateProduct()
    {
        $params = Request::put();
        $params['main_img_url'] = explode(config('setting.img_prefix'), $params['main_img_url'])[1];
        ProductModel::update($params);
        return writeJson(201, '商品信息更新成功');
    }

    /**
     * 添加商品详情图
     * @validate('ProductImageForm')
     */
    public function addProductImage()
    {
        $params = Request::post('image');
        (new ProductImageModel())->saveAll($params);

        return writeJson(201, '商品详情图新增成功');
    }

    /**
     * 编辑商品详情图
     * @validate('ProductImageForm.edit')
     * @param('image','商品详情图数组','require|array|min:1')
     */
    public function updateProductImage()
    {
        $params = Request::put('image');
        (new ProductImageModel())->saveAll($params);

        return writeJson(201, '商品详情图编辑成功');
    }

    /**
     * 删除商品详情图
     * @param('ids','待删除的商品详情图id列表','require|array|min:1')
     */
    public function delProductImage()
    {
        $ids = Request::delete('ids');
        ProductImageModel::destroy($ids);
        return writeJson(201, '商品详情图删除成功');
    }

    /**
     * 添加商品的商品属性
     * @validate('ProductPropertyForm')
     */
    public function addProductProperty()
    {
        $params = Request::post('property');
        (new ProductPropertyModel())->saveAll($params);

        return writeJson(201, '商品属性新增成功');
    }

    /**
     * 编辑商品属性
     * @validate('ProductPropertyForm.edit')
     */
    public function updateProductProperty()
    {
        $params = Request::put('property');
        (new ProductPropertyModel())->saveAll($params);

        return writeJson(201, '商品属性编辑成功');
    }

    /**
     * 删除商品属性
     * @param('ids','待删除的商品属性id列表','require|array|min:1')
     */
    public function delProductProperty()
    {
        $ids = Request::delete('ids');
        ProductPropertyModel::destroy($ids);
        return writeJson(201, '商品属性删除成功');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

这里我们一口气新增了7个控制器方法,气势有了,但内容朴实无华,接下来让我们来一一解读一下:

  • updateProduct()
    /**
     * 更新商品基础信息
     * @validate('ProductForm.edit')
     */
    public function updateProduct()
    {
        $params = Request::put();
        $params['main_img_url'] = explode(config('setting.img_prefix'), $params['main_img_url'])[1];
        ProductModel::update($params);
        return writeJson(201, '商品信息更新成功');
    }
1
2
3
4
5
6
7
8
9
10
11

首先实现的是对商品基础信息的修改,这里我们给接口添加了一个注解验证器,验证器类复用了前面新增商品接口所使用的验证器类,只不过增加了场景验证,验证内容只是做了稍微的修改,让我们看一下编辑场景的验证规则变化:

<?php


namespace app\api\validate\product;


use LinCmsTp5\validate\BaseValidate;

class ProductForm extends BaseValidate
{
    protected $rule = [
        'name' => 'require',
        'category_id' => 'number',
        'img_id' => 'require|number',
        'main_img_url' => 'require|url',
        'price' => 'require|float',
        'stock' => 'require|number',
        'summary' => 'chsDash',
        'image' => 'array|productImage',
        'property' => 'array|productProperty',
    ];

    // 场景声明
    public function sceneEdit()
    {
        return $this->append('id', ['require', 'number']);
    }

    protected function productImage($value){...}

    protected function productProperty($value){...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

这里我们定义了在编辑场景下需要额外传入一个商品id的验证规则,因为后面在做更新操作的时候模型的方法需要根据商品的主键id来实现更新。回到我们的控制器方法中:

    /**
     * 更新商品基础信息
     * @validate('ProductForm.edit')
     */
    public function updateProduct()
    {
        $params = Request::put();
        $params['main_img_url'] = explode(config('setting.img_prefix'), $params['main_img_url'])[1];
        ProductModel::update($params);
        return writeJson(201, '商品信息更新成功');
    }
1
2
3
4
5
6
7
8
9
10
11

这里我们同样是先获取了参数,然后对参数中的main_img_url字段做一些格式化的操作,然后把$params直接传递了模型的update()方法实现商品记录的更新操作。方法内容很简单,接着我们来定义一条路由规则测试一下这个接口,打开route.php,在product路由分组下新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            // 分页查询所有商品
            Route::get('paginate', 'api/v1.Product/getProductsPaginate');
            // 查询所有可用商品
            Route::get('', 'api/v1.Product/getProducts');
            // 商品上架/下架
            Route::patch(':id','api/v1.Product/modifyStatus');
            // 新增商品
            Route::post('','api/v1.Product/addProduct');
            // 删除商品
            Route::delete('','api/v1.Product/delProduct');
            // 更新商品基础信息
            Route::put('','api/v1.Product/updateProduct');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

路由定义完之后,打开Postman按照路由规则配置并新增一个请求:

在下方空白输入区域粘贴以下json数据:

{
	"id":1,
	"name":"芹菜 万斤",
	"price":"0.01",
	"stock":998,
	"category_id":3,
	"main_img_url":"http://localhost:8000/images/product-vg@1.png",
	"status":1,
	"summary":null,
	"img_id":13
}
1
2
3
4
5
6
7
8
9
10
11

这里我们把商品id为1的商品名称做了修改,点击发送:

{
    "error_code": 0,
    "result": "商品信息更新成功",
    "msg": "ok"
}
1
2
3
4
5

没有报错,显示修改成功了,接着我们调用一下分页查询接口来验证一下:

{
    "collection": [
        {
            "id": 1,
            "name": "芹菜 万斤",
            "price": "0.01",
            "stock": 998,
            "category_id": 3,
            "main_img_url": "http://localhost:8000/images/product-vg@1.png",
            "status": 1,
            "summary": null,
            "img_id": 13,
            // 省略部分字段内容
        }
    ],
    "total_nums": 33
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

没有问题,这里的芹菜由原来的半斤变成了万斤,而且价格不变,简直壕无人性。读者可以继续尝试修改其他字段的内容来实现商品基本信息的修改。


  • addProductImage()、updateProductImage()、delProductImage()
    /**
     * 添加商品详情图
     * @validate('ProductImageForm')
     */
    public function addProductImage()
    {
        $params = Request::post('image');
        (new ProductImageModel())->saveAll($params);

        return writeJson(201, '商品详情图新增成功');
    }

    /**
     * 编辑商品详情图
     * @validate('ProductImageForm.edit')
     */
    public function updateProductImage()
    {
        $params = Request::put('image');
        (new ProductImageModel())->saveAll($params);

        return writeJson(201, '商品详情图编辑成功');
    }

    /**
     * 删除商品详情图
     * @param('ids','待删除的商品详情图id列表','require|array|min:1')
     */
    public function delProductImage()
    {
        $ids = Request::delete('ids');
        ProductImageModel::destroy($ids);
        return writeJson(201, '商品详情图删除成功');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

addProductImage()updateProductImage()delProductImage()这三个接口用于对商品详情图的管理操作,对应新增、编辑、删除,利用模型方法删除数据相信大家已经非常熟悉了这里就不多做介绍,这里主要讲解一下关于新增和编辑接口的一些知识点。从代码中我们可以看到,这两个控制器方法内的实现基本是一模一样的,从实现手段上来说,模型的savaAll()方法既可以用于数据批量新增也可以用于数据批量更新,框架会自动识别,识别的方式就是你传递的数组元素中有没有包含主键id字段,如果有,框架就认为你是更新操作,如果没有就是新增,那么读者就有会疑问,既然框架会自动识别,何不把两个方法合并成一个方法?确实可以,但不推荐这么做,在教程中我们这里只是简单的对数据进行了新增和更新,可以考虑把两个接口合并成一个,但是在真实项目中,我们往往不止是做简单的新增和更新操作而已,那就涉及到需要针对不同场景的做业务逻辑判断,比如说这里要给接口加一个行为日志,那么行为日志的内容就要记录本次操作是新增和更新,为了拿到这个信息,我们又得写多几行代码。所以更推荐的做法还是把接口拆分成两个独立的接口方便日后扩展。了解完这里的设计初衷之后,让我们继续回到代码中,这里我们给三个接口都添加了注解验证,其中删除商品详情图接口用了注解参数,内容含义相信大家也已经很熟悉并掌握了,重点看新增和编辑商品详情图接口的注解验证器:

    /**
     * 添加商品详情图
     * @validate('ProductImageForm')
     */
    public function addProductImage(){...}

    /**
     * 编辑商品详情图
     * @validate('ProductImageForm.edit')
     */
    public function updateProductImage(){...}

1
2
3
4
5
6
7
8
9
10
11
12

这里我们定义了一个ProductImageForm验证器类,并声明了一个edit的场景验证给编辑商品详情图接口,这里我们来创建一下这个验证器类,在项目根目录下的appliction\api\validate\product新增一个ProductImageForm.php文件,并加入以下代码:

<?php


namespace app\api\validate\product;


use LinCmsTp5\validate\BaseValidate;

class ProductImageForm extends BaseValidate
{
    protected $rule = [
        'image' => 'require|array|min:1|productImage',
    ];

    public function sceneEdit()
    {
        return $this->remove('image', 'productImage')
            ->append('image', 'requireId');
    }

    protected function productImage($value)
    {
        foreach ($value as $v) {
            if (!isset($v['product_id']) || empty($v['product_id'])) {
                return '商品详情图所属商品id不能为空';
            }

            if (!isset($v['img_id']) || empty($v['img_id'])) {
                return '商品详情图不能为空';
            }
        }
        return true;
    }

    protected function requireId($value)
    {
        foreach ($value as $v) {
            if (!isset($v['id']) || empty($v['id'])) {
                return '商品详情图主键id不能为空';
            }

            if (!isset($v['product_id']) || empty($v['product_id'])) {
                return '商品详情图所属商品id不能为空';
            }

            if (!isset($v['img_id']) || empty($v['img_id'])) {
                return '商品详情图不能为空';
            }
        }
        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

我们定义了productImagerequireId两个自定义验证规则。当触发编辑场景时,我们移除默认的productImage验证规则,改用requireId验证规则,因为编辑商品详情图时需要多传递一个商品详情图记录的主键id。

验证器定义完毕之后,让我们来给这三个接口定义路由规则,打开route.php文件,在product路由分组下定义三条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            .........................
            .........................
            // 新增商品详情图
            Route::post('image','api/v1.Product/addProductImage');
            // 编辑商品详情图
            Route::put('image','api/v1.Product/updateProductImage');
            // 删除商品详情图
            Route::delete('image','api/v1.Product/delProductImage');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

路由规则定义好之后,打开Postman按路由规则配置并新增三个请求。

注意测试过程中作者传递的参数可能会与读者当前数据库中的情况不一致,请读者务必先通过查询接口查询数据后再用于后续测试:

首先是新增商品详情图,这里我们来为id为1的商品新增一个商品详情图,传入json参数:

{
	"image":[
		{
			"img_id":"160",
			"order":0,
			"product_id":1
		}
	]
}
1
2
3
4
5
6
7
8
9

接着是编辑商品详情图,这里我们尝试改变id为34的商品详情图的order字段,就是排序,传入json参数:

{
	"image":[
		{
			"img_id":149,
			"order":1,
			"product_id":1,
			"id":34
		}
	]
}
1
2
3
4
5
6
7
8
9
10

最后,我们尝试删除id为35的商品详情图,传入json参数:

{
	"ids":[35]
}
1
2
3

以上测试均基于id为1的商品,读者可自行参照作者的示例进行测试,最后调用查询接口验证相关数据是否正常被修改。


  • addProductProperty()、updateProductProperty()、delProductProperty()
    /**
     * 添加商品的商品属性
     * @validate('ProductPropertyForm')
     * @return \think\response\Json
     * @throws \Exception
     */
    public function addProductProperty()
    {
        $params = Request::post('property');
        (new ProductPropertyModel())->saveAll($params);

        return writeJson(201, '商品属性新增成功');
    }

    /**
     * 编辑商品属性
     * @validate('ProductPropertyForm.edit')
     * @return \think\response\Json
     * @throws \Exception
     */
    public function updateProductProperty()
    {
        $params = Request::put('property');
        (new ProductPropertyModel())->saveAll($params);

        return writeJson(201, '商品属性编辑成功');
    }

    /**
     * 删除商品属性
     * @param('ids','待删除的商品属性id列表','require|array|min:1')
     * @return \think\response\Json
     */
    public function delProductProperty()
    {
        $ids = Request::delete('ids');
        ProductPropertyModel::destroy($ids);
        return writeJson(201, '商品属性删除成功');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

addProductProperty()updateProductProperty()delProductProperty()这三个接口用于对商品属性的管理操作,对应新增、编辑、删除,具体的实现和思路与前面商品详情图是一致的,在前面ProductImageForm验证器的同目录下新建一个ProductPropertyForm.php文件并添加如下代码:

<?php


namespace app\api\validate\product;


use LinCmsTp5\validate\BaseValidate;

class ProductPropertyForm extends BaseValidate
{
   protected $rule = [
       'property' => 'require|array|min:1|productProperty',
   ];

   public function sceneEdit()
   {
       return $this->remove('property', 'productProperty')
           ->append('property', 'requireId');
   }

   protected function productProperty($value)
   {
       if (!empty($value)) {
           foreach ($value as $v) {
               if (!isset($v['product_id']) || empty($v['product_id'])) {
                   return '商品属性所属商品id不能为空';
               }

               if (!isset($v['name']) || empty($v['name'])) {
                   return '商品属性名称不能为空';
               }
               if (!isset($v['detail']) || empty($v['detail'])) {
                   return '商品属性' . $v['name'] . '的详情不能为空';
               }
           }
       }

       return true;
   }

   protected function requireId($value)
   {
       foreach ($value as $v) {
           if (!isset($v['id']) || empty($v['id'])) {
               return '商品属性主键id不能为空';
           }

           if (!isset($v['name']) || empty($v['name'])) {
               return '商品属性名称不能为空';
           }
           if (!isset($v['detail']) || empty($v['detail'])) {
               return '商品属性' . $v['name'] . '的详情不能为空';
           }
       }
       return true;
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

基本都是如法炮制,验证器定义完毕之后,让我们来给这三个接口定义路由规则,打开route.php文件,在product路由分组下定义三条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        ............................
        ............................
        ............................
        // 商品管理相关接口
        Route::group('product', function () {
            .........................
            .........................
            // 新增商品属性
            Route::post('property','api/v1.Product/addProductProperty');
            // 编辑商品属性
            Route::put('property','api/v1.Product/updateProductProperty');
            // 删除商品属性
            Route::delete('property','api/v1.Product/delProductProperty');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

路由规则定义好之后,打开Postman按路由规则配置并新增三个请求。

注意测试过程中作者传递的参数可能会与读者当前数据库中的情况不一致,请读者务必先通过查询接口查询数据后再用于后续测试:

首先是新增商品属性,这里我们来为id为1的商品新增一个叫爱好的商品属性,传入json参数:

{
	"property":[
		{
			"name":"爱好",
			"detail":"不详",
			"product_id":1
		}
	]
}
1
2
3
4
5
6
7
8
9

接着是编辑商品属性,这里我们尝试改变id为19的商品属性的detail字段,传入json参数:

{
	"property":[
		{
			"id":19,
			"name":"搜索",
			"detail":"哈哈",
			"product_id":1
		}
	]
}
1
2
3
4
5
6
7
8
9
10

最后,我们尝试删除id为21、22的商品属性,传入json参数:

{
	"ids":[21,22]
}
1
2
3

以上测试均基于id为1的商品,读者可自行参照作者的示例进行测试,最后调用查询接口验证相关数据是否正常被修改。

# 章节回顾

通过本章节的学习,我们实现了商品库管理的一些系列接口,包括商品的新增、编辑、删除。如同章节一开始中提到的一样,商品是目前我们原型APP业务体系中最核心也是最基础的数据,实现了这一部分的管理才能支撑后期业务的开展和实施运营、营销等。通过实现功能的过程,我们接触和实践了“商品”这类数据如果利用多种关联关系实现与不同业务模块之间灵活地数据联动,这一部分的知识点才是最重要的,很多时候我们的系统变得不可维护和难扩展问题根源还是在于最开始的架构设计阶段然后才是具体代码的编写,希望以上本章节的内容能够给读者带来一些对这方面的启发,从下一章节开始,我们将从另一个维度着手去开发实现一系列管理接口,比如订单管理、会员管理等。

最后更新: 2021-08-12 13:31:59
0/140
评论
0
暂无评论
  • 上一页
  • 首页
  • 1
  • 尾页
  • 下一页
  • 总共1页